﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CGSR.Core
{
    public class SimulationManager
    {
        #region Properties

        public int receptionRadius;
        public int NumOfTicks;
		public int currentTick = 0;

    	private double[][] distances;

		
        /// These are the parameters to compute power for sending
        private double C = 2;
        private double K = 0.001;

        /// <summary>
        /// The nodes in the simulation. A dictionary between the id and the corresponding node
        /// </summary>
        public Node[] Nodes { get; private set; }

        /// <summary>
        /// When the simulation manager receives a packet to transfer to another node, it tranfers it only after all nodes
        /// are done sending packets. 
        /// This emposes the one tick delay limit per hop.
        /// </summary>
        List<Packet> packetsToTransfer = new List<Packet>();

		StreamWriter file;

        #endregion

        #region C'tors

        public SimulationManager()
        {
        	
        }

    	#endregion

        #region Public Methods

        public Tuple<double,double> RunSimulation(SimulationLocationsManager locationsManager, SimulationPacketsManager packetsManager, string outputFile)
        {
			file = new StreamWriter(outputFile, false);
			file.WriteLine("Packet ID, Sent on tick, Revieved?, Recieved on tick");

        	NumOfTicks = locationsManager.NumOfTicks;
            receptionRadius = packetsManager.ReceptionRadius;
			Nodes=new Node[locationsManager.NumOfUsers];

            /// Initialize locations for the simulation run
            Location[] initialLocations = locationsManager.GetLocationsForNextTick();
            for (int i = 0; i < initialLocations.Length; i++)
            {
                Location loc = initialLocations[i];
                Nodes[i] = new Node(i, loc.X, loc.Y, packetsManager.Power, packetsManager.QueueSize, this);
            }
        	updateDistances();
            /// Perform Initial Clustering
            InitialClustering();

            selectGateways();

            // each CH need to know how to get to each other CH,
            // and to know every node's CH, so when they need to send a message to a non-CH node, they'll know wich CH to look for
            UpdateRoutingTables();

            AddTraficToQueues(packetsManager.GetTrafficForNextTick());

            foreach (var node in Nodes)
            {
                node.TransferPackets();
            }

            foreach (var packet in packetsToTransfer)
            {
                Nodes[packet.NextHop].ReceivePacket(packet);
            }

            // Start simulation, and for each Tick perform actions: Moving, Sending packets.
			
        	currentTick = 1;
        	while (currentTick < NumOfTicks)
            {
				UpdateLocations(locationsManager.GetLocationsForNextTick());

            	recluster();

				selectGateways();

				UpdateRoutingTables();

				foreach (Node n in Nodes)
                {
                    n.NewTick();
                }

                AddTraficToQueues(packetsManager.GetTrafficForNextTick());

                foreach (var node in Nodes)
                {
                    node.TransferPackets();
                }

                foreach (var packet in packetsToTransfer)
                {
                    Nodes[packet.NextHop].ReceivePacket(packet);
                }
				packetsToTransfer.Clear();

				if (currentTick%100==0)
            		Console.WriteLine("		Finished tick "+currentTick+"/"+NumOfTicks);

				currentTick++;

            }

            foreach (var node in Nodes)
        	{
        		node.dropAllPackets();
        	}

			file.Close();
			return new Tuple<double, double>(lostPackets/(lostPackets+receivedPackets),totalDelay/receivedPackets);
        }

    	private void recluster()
    	{
			for (int i = 0; i < Nodes.Length; i++)
    		{
    			Nodes[i].ReclusterPhaseOne();
    		}

			for (int i = 0; i < Nodes.Length; i++)
			{
				Nodes[i].ReclusterPhaseTwo();
			}    		
    	}


		public double calcDistanceBetweenNodes(Node n1, Node n2)
		{
			int xDist = n1.X - n2.X;
			if (xDist > receptionRadius)
				return xDist; // anyway out of reception radius

			int yDist = n1.Y - n2.Y;
			if (yDist > receptionRadius)
				return yDist; // anyway out of reception radius

			return (xDist.sqr() + (n1.Y - n2.Y).sqr()).sqrt();
		}

    	/// <summary>
        /// Returns the distance between 2 nodes
        /// </summary>
        /// <param name="n1"></param>
        /// <param name="n2"></param>
        /// <returns></returns>
        public double GetDistanceBetweenNodes(Node n1, Node n2)
    	{
    		return GetDistanceBetweenNodes(n1.ID, n2.ID);
    	}

        /// <summary>
        /// Returns the distance between 2 nodes by their IDs
        /// </summary>
        /// <param name="n1"></param>
        /// <param name="n2"></param>
        /// <returns></returns>
        public double GetDistanceBetweenNodes(int n1ID, int n2ID)
        {
			if (n1ID < n2ID)
				return distances[n1ID][n2ID];
			else
				if (n1ID > n2ID)
					return distances[n2ID][n1ID];
				else
					return 0;
        }

        /// <summary>
        /// This method computes the power it takes to send a message in the given distance
        /// </summary>
        /// <param name="distance"></param>
        /// <returns></returns>
        public double ComputeNeededPowerForSendingPacket(double distance)
        {
            return this.K * Math.Pow(distance, this.C);
        }

        #endregion

        #region Clustering

        private void InitialClustering()
        {
            for (int i = 0; i < Nodes.Length; i++)
            {
                Nodes[i].ClusterPhaseOne();
            }

			for (int i = 0; i < Nodes.Length; i++)
            {
                Nodes[i].ClusterPhaseTwo();
            }
        }

        public void Probe(Node probingNode, List<int> acks, int? clusterHead)
        {
            /// Get all the nodes i can hear and can hear me
            IEnumerable<Node> nodesToProbe = Nodes.Where(n => n != probingNode && probingNode.CanHear(n));

            /// The broadcast takes power
            double powerNeeded = ComputeNeededPowerForSendingPacket(this.receptionRadius);
            if (powerNeeded <= probingNode.Power)
            {
                probingNode.Power -= powerNeeded;

                foreach (Node node in nodesToProbe)
                {
                    node.Probed(probingNode, clusterHead);
                }
            }
        }

        private void selectGateways()
        {
            IEnumerable<Node> notCHs = Nodes.Where(x => !x.IsCH);
            foreach (Node node in notCHs)
            {
                node.broadcastCHs();
            }

            foreach (Node node in notCHs)
            {
                node.broadcastReachableCHs();
            }
        }

		#region broadcasts
		public void broadcastReachableCHs(Node source, IEnumerable<int> reachableCHs, IEnumerable<int> destinations)
        {
            double powerNeeded = ComputeNeededPowerForSendingPacket(this.receptionRadius);
            source.Power -= powerNeeded;
            foreach (var dest in destinations)
            {
                Nodes[dest].UpdatePossibleGW(source.ID, reachableCHs);
            }            
        }

        internal void broadcastCHs(Node source, IEnumerable<int> reachableCHs, IEnumerable<int> destinations)
        {
            double powerNeeded = ComputeNeededPowerForSendingPacket(this.receptionRadius);
            source.Power -= powerNeeded;
            foreach (int dest in destinations)
            {
                Nodes[dest].UpdateTwoHopCHs(source.ID, reachableCHs);
            }
        }

		public void broadcastNewCH(Node source, int clusterHead)
		{
			double powerNeeded = ComputeNeededPowerForSendingPacket(this.receptionRadius);
			source.Power -= powerNeeded;
			foreach (var dest in source.newneighbors)
			{
				dest.UpdateNewCH(source.ID, clusterHead);
			}
		}
		#endregion

        private void UpdateRoutingTables()
        {
            foreach (Node node in Nodes.Where(x => x.IsCH))
            {
                node.RecalcRoutingTables();
            }

			foreach (Node node in Nodes.Where(x => x.isRoutingTablesDirty))
            {
                    node.PropagateRoutingTables();
            }

           
        }

        #endregion


        #region Routing

        private void UpdateLocations(Location[] locations)
        {
            for (int i = 0; i < locations.Length; i++)
            {
                Nodes[i].Move(locations[i]);
            }
			updateDistances();
        }

    	private void updateDistances()
    	{
    		distances = new double[Nodes.Length][];
    		
    		for (int i = 0; i < Nodes.Length; i++)
    		{
				distances[i] = new double[Nodes.Length];
    			Parallel.For(0,Nodes.Length,j=>distances[i][j] = calcDistanceBetweenNodes(Nodes[i], Nodes[j]));
//				for (int j = i+1; j < Nodes.Length; j++)
//    			{
//    				distances[i][j] = calcDistanceBetweenNodes(Nodes[i], Nodes[j]);
//    			}
    		}
    	}

    	private void AddTraficToQueues(ICollection<Packet> traffic)
        {
            foreach (Packet p in traffic)
            {
                Node sender = this.Nodes[p.Source];
                sender.EnqueuePacket(p);
            }
        }

		#region statistics collection

    	private double receivedPackets = 0;
		private int lostPackets = 0;
		private int totalDelay = 0;

		/// <summary>
        /// A packet reached its' destination. Collect stats and write to output
        /// </summary>
        /// <param name="node"></param>
        /// <param name="packet"></param>
        internal void PacketReceived(Node node, Packet packet)
        {
			if (!(packet is RoutingInfoPacket))
			{
				receivedPackets++;
				totalDelay += currentTick - packet.SentOnTick;
				file.WriteLine(packet.ID + "," + packet.SentOnTick + ",yes," + currentTick);
				//Console.WriteLine("Tick " + CurrentTick + ", packet received from " + packet.Source + " to " + packet.Destination);
			}
        }

        /// <summary>
        /// A packet failed to reach its' destination. Collect stats and write to output
        /// </summary>
        /// <param name="node"></param>
        /// <param name="packet"></param>
        internal void PacketFailedToReachDestination(Packet packet)
        {
            if (!(packet is RoutingInfoPacket))
            {
            	lostPackets++;
				//Console.WriteLine("packet lost");
				file.WriteLine(packet.ID + "," + packet.SentOnTick + ",no, packet lost");
			}
        }
		#endregion


        /// <summary>
        /// 
        /// </summary>
        /// <param name="packet"></param>
        /// <param name="node"></param>
        internal void SendPacket(Packet packet, Node sender)
        {
            // Sanity check only, this should never be true:
            if (!sender.CanHear(packet.NextHop))
            {
               throw new Exception("Tried to send a message to a node out of reception radius");
            }
            
            packetsToTransfer.Add(packet);
            
        }

        #endregion

    	public List<Node> neighbors(Node node)
    	{
			var retVal = new List<Node>();
    		foreach (var node1 in Nodes)
    		{
    			if (node.CanHear(node1))
					retVal.Add(node1);
    		}
    		return retVal;
    	}

    	
    }
}
